Skip to content

gh-157242: Leave bytearray unchanged if resize() fails - #157243

Open
vstinner wants to merge 7 commits into
python:mainfrom
vstinner:writer_resize2
Open

gh-157242: Leave bytearray unchanged if resize() fails#157243
vstinner wants to merge 7 commits into
python:mainfrom
vstinner:writer_resize2

Conversation

@vstinner

@vstinner vstinner commented Sep 10, 2026

Copy link
Copy Markdown
Member

If bytearray.resize() or bytearray.take_bytes() fails, leave the bytearray unchanged.

If PyBytesWriter_Resize() fails, leave the writer unchanged.

Add a new internal _PyBytes_ResizeKeepOnError() function similar to _PyBytes_Resize() but leaves the bytes object unchanged on error.

If bytearray.resize() or bytearray.take_bytes() fails, leave the
bytearray unchanged.

If PyBytesWriter_Resize() fails, leave the writer unchanged.

Add a new internal _PyBytes_ResizeKeepOnError() function similar to
_PyBytes_Resize() but leaves the bytes object unchanged on error.
Comment thread Objects/bytesobject.c Outdated
Comment thread Objects/bytearrayobject.c Outdated
@cmaloney

Copy link
Copy Markdown
Contributor

It would be nice if the "guaranteed no global" case was also used for the PyBytes_FromStringAndSize(NULL, size); call. That the first parameter there must be NULL to avoid a global is unintuitive.

@serhiy-storchaka serhiy-storchaka left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to add a new parameter, it saves nothing. _PyBytes_Resize can be implemented via _PyBytes_ResizeKeepOnError.

I wonder if we can simply change the behavior of _PyBytes_Resize.

Comment thread Objects/bytearrayobject.c Outdated
vstinner and others added 2 commits September 10, 2026 10:39
Co-authored-by: Maurycy Pawłowski-Wieroński <maurycy@maurycy.com>
For the in-place resize code path, no longer call
_Py_ForgetReference() and _PyReftracerTrack() before
PyObject_Realloc().
@vstinner

Copy link
Copy Markdown
Member Author

Please review the updated PR. I addressed reviews.

@maurycy found a fix for the memmove() code path which worried me. I applied his suggestion and added a test.

@serhiy-storchaka:

No need to add a new parameter, it saves nothing. _PyBytes_Resize can be implemented via _PyBytes_ResizeKeepOnError. I wonder if we can simply change the behavior of _PyBytes_Resize.

Thanks for the advice. I reworked _PyBytes_Resize(): for the in-place resize code path, no longer call _Py_ForgetReference() and _PyReftracerTrack() before PyObject_Realloc(). Only call them on success. With this change, I was able to easy implement _PyBytes_Resize() with _PyBytes_ResizeKeepOnError().

@vstinner

Copy link
Copy Markdown
Member Author

It would be nice if the "guaranteed no global" case was also used for the PyBytes_FromStringAndSize(NULL, size); call. That the first parameter there must be NULL to avoid a global is unintuitive.

Maybe PyBytes_FromStringAndSize() documentation should be elaborated to explain that PyBytes_FromStringAndSize(NULL, size) can be mutated, whereas PyBytes_FromStringAndSize(str, size) must not be mutated?

Note: PR gh-156996 does fix PyBytes_FromStringAndSize() usage in bytearray. I don't try to replace this fix.

@vstinner

Copy link
Copy Markdown
Member Author

bytearray.resize() and bytearray.take_bytes() have been fixed to no longer use a singleton: I merged main in my PR to get the PR gh-156996 fix.

Comment thread Objects/bytearrayobject.c Outdated
@vstinner

Copy link
Copy Markdown
Member Author

@maurycy: I modified resize() and take_bytes() to avoid memmove() usage if we would be unable to revert the bytesarray to its previous state on MemoryError. Does it look correct to you?

I also added more tests injecting MemoryError.

@maurycy

maurycy commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

@vstinner: Thank you. I will take a look more carefully later today.

Comment thread Lib/test/test_bytes.py
with self.assertRaises(MemoryError):
try:
_testcapi.set_nomemory(mem_error)
ba.take_bytes(5)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ba.take_bytes(5)
ba.take_bytes(to_take)

if I understand the intent correctly

Comment thread Objects/bytesobject.c
Comment on lines 3403 to 3406
#ifdef Py_TRACE_REFS
_Py_ForgetReference(v);
#endif
_PyReftracerTrack(v, PyRefTracer_DESTROY);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But v is now freed by PyObject_Realloc()? But ASan should've caught this?

Comment thread Objects/bytearrayobject.c
the number of bytes being removed in a resize is small; for large
size changes it may be better to just make a new bytes object as
_PyBytes_Resize will do a malloc + memcpy internally. */
memmove(obj->ob_bytes, obj->ob_start, Py_SIZE(self));

@maurycy maurycy Sep 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
memmove(obj->ob_bytes, obj->ob_start, Py_SIZE(self));
memmove(obj->ob_bytes, obj->ob_start, Py_SIZE(self));
obj->ob_bytes[Py_SIZE(self)] = '\0';

Do we care about this?

2026-09-10T14:51:02.756952000+0200 maurycy@gimel /Users/maurycy/work/cpython-pr157243 (pr157243 f51cba5?) % ./python.exe
Python 3.16.0a0 (heads/pr157243:f51cba57d3e, Sep 10 2026, 14:31:04) [Clang 21.0.0 (clang-2100.1.1.101)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import _testcapi, _testlimitedcapi
>>> ba = bytearray(b'0123456789')
>>> del ba[:3]
>>> ba
bytearray(b'3456789')
>>> try: _testcapi.set_nomemory(0); ba.resize(1024)
... except MemoryError: _testcapi.remove_mem_hooks()
... 
>>> ba
bytearray(b'3456789')
>>> _testlimitedcapi.bytearray_asstring(ba, len(ba) + 1)
bytearray(b'34567897')
>>> _testlimitedcapi.bytearray_asstring(ba, len(ba) + 100)
bytearray(b'3456789789\x00\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\x00\x00\x00\x00\x00\x00\x008o\xfd\xfd\xfd\xfd\xfd\xfd\xfd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb0\xdd\xcd\x08\x01\x00\x00\x00@%\\\x03\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfd\xfd\xfd')
>>> 

This is so subtle :(

There's a similar isue:

https://github.com/python/cpython/pull/157243/changes#diff-6878f29bbfb711dc7cd2a89b2b2941b1f900b6367d30181a2ff45460b591b04cR1650

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting core review needs backport to 3.15 pre-release feature fixes, bugs and security fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants